<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgl - gpgpu - protoplanet</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<style>
			body {
				background-color: #000000;
				margin: 0px;
				overflow: hidden;
				font-family:Monospace;
				font-size:13px;
				text-align:center;
				text-align:center;
				cursor: pointer;
			}

			a {
				color:#0078ff;
			}

			#info {
				color: #ffffff;
				position: absolute;
				top: 10px;
				width: 100%;
			}

		</style>
	</head>
	<body>

		<div id="info">
			<a href="http://threejs.org" target="_blank">three.js</a> - <span id="protoplanets"></span> webgl gpgpu debris<br/>
			Select <span id="options"></span> debris<br/>

		</div>

		<script src="../build/three.js"></script>
		<script src="js/Detector.js"></script>
		<script src="js/libs/stats.min.js"></script>
		<script src="js/libs/dat.gui.min.js"></script>
		<script src="js/controls/OrbitControls.js"></script>

		<script src="js/GPUComputationRenderer.js"></script>


		<!-- Fragment shader for protoplanet's position -->
		<script id="computeShaderPosition" type="x-shader/x-fragment">

			#define delta ( 1.0 / 60.0 )

			void main() {

				vec2 uv = gl_FragCoord.xy / resolution.xy;

				vec4 tmpPos = texture2D( texturePosition, uv );
				vec3 pos = tmpPos.xyz;

				vec4 tmpVel = texture2D( textureVelocity, uv );
				vec3 vel = tmpVel.xyz;
				float mass = tmpVel.w;

				if ( mass == 0.0 ) {
					vel = vec3( 0.0 );
				}

				// Dynamics
			        pos += vel * delta;

				gl_FragColor = vec4( pos, 1.0 );

			}

		</script>

		<!-- Fragment shader for protoplanet's velocity -->
		<script id="computeShaderVelocity" type="x-shader/x-fragment">

			// For PI declaration:
			#include <common>

			#define delta ( 1.0 / 60.0 )

			uniform float gravityConstant;
			uniform float density;

			const float width = resolution.x;
			const float height = resolution.y;

			float radiusFromMass( float mass ) {
				// Calculate radius of a sphere from mass and density
				return pow( ( 3.0 / ( 4.0 * PI ) ) * mass / density, 1.0 / 3.0 );
			}

			void main()	{

				vec2 uv = gl_FragCoord.xy / resolution.xy;
				float idParticle = uv.y * resolution.x + uv.x;

				vec4 tmpPos = texture2D( texturePosition, uv );
				vec3 pos = tmpPos.xyz;

				vec4 tmpVel = texture2D( textureVelocity, uv );
				vec3 vel = tmpVel.xyz;
				float mass = tmpVel.w;

				if ( mass > 0.0 ) {
				    

					float radius = radiusFromMass( mass );

					vec3 acceleration = vec3( 0.0 );

					// Gravity interaction
					for ( float y = 0.0; y < height; y++ ) {

						for ( float x = 0.0; x < width; x++ ) {

							vec2 secondParticleCoords = vec2( x + 0.5, y + 0.5 ) / resolution.xy;
							vec3 pos2 = texture2D( texturePosition, secondParticleCoords ).xyz;
							vec4 velTemp2 = texture2D( textureVelocity, secondParticleCoords );
							vec3 vel2 = velTemp2.xyz;
							float mass2 = velTemp2.w;

							float idParticle2 = secondParticleCoords.y * resolution.x + secondParticleCoords.x;

							if ( idParticle == idParticle2 ) {
								continue;
							}

							if ( mass2 == 0.0 ) {
								continue;
							}

							vec3 dPos = pos2 - pos;
							float distance = length( dPos );
							float radius2 = radiusFromMass( mass2 );

							if ( distance == 0.0 ) {
								continue;
							}

							// Checks collision

							if ( distance < radius + radius2 ) {

								if ( idParticle < idParticle2 ) {

									// This particle is aggregated by the other
									vel = ( vel * mass + vel2 * mass2 ) / ( mass + mass2 );
									mass += mass2;
									radius = radiusFromMass( mass );

								}
								else {

									// This particle dies
									mass = 0.0;
									radius = 0.0;
									vel = vec3( 0.0 );
									break;

								}

							}

							float distanceSq = distance * distance;

							float gravityField = gravityConstant * mass2 / distanceSq;

							gravityField = min( gravityField, 1000.0 );

							acceleration += gravityField * normalize( dPos );

						}

						if ( mass == 0.0 ) {
							break;
						}
					}

					// Dynamics
					vel += delta * acceleration;

				}

				gl_FragColor = vec4( vel, mass );

			}

		</script>

		<!-- Particles vertex shader -->
		<script type="x-shader/x-vertex" id="particleVertexShader">

			// For PI declaration:
			#include <common>

			uniform sampler2D texturePosition;
			uniform sampler2D textureVelocity;

			uniform float cameraConstant;
			uniform float density;

			varying vec4 vColor;

			float radiusFromMass( float mass ) {
				// Calculate radius of a sphere from mass and density
				return pow( ( 3.0 / ( 4.0 * PI ) ) * mass / density, 1.0 / 3.0 );
			}


			void main() {


				vec4 posTemp = texture2D( texturePosition, uv );
				vec3 pos = posTemp.xyz;

				vec4 velTemp = texture2D( textureVelocity, uv );
				vec3 vel = velTemp.xyz;
				float mass = velTemp.w;

				vColor = vec4( 1.0, mass / 250.0, 0.0, 1.0 );

				vec4 mvPosition = modelViewMatrix * vec4( pos, 1.0 );

				// Calculate radius of a sphere from mass and density
				//float radius = pow( ( 3.0 / ( 4.0 * PI ) ) * mass / density, 1.0 / 3.0 );
				float radius = radiusFromMass( mass );

				// Apparent size in pixels
				if ( mass == 0.0 ) {
					gl_PointSize = 0.0;
				}
				else {
					gl_PointSize = radius * cameraConstant / ( - mvPosition.z );
				}

				gl_Position = projectionMatrix * mvPosition;

			}

		</script>

		<!-- Particles fragment shader -->
		<script type="x-shader/x-fragment" id="particleFragmentShader">

			varying vec4 vColor;

			void main() {

				float f = length( gl_PointCoord - vec2( 0.5, 0.5 ) );
				if ( f > 0.5 ) {
					discard;
				}
				gl_FragColor = vColor;

			}

		</script>


		<script>

			if ( ! Detector.webgl ) Detector.addGetWebGLMessage();

			var hash = document.location.hash.substr( 1 );
			if ( hash ) hash = parseInt( hash, 0 );

			// Texture width for simulation (each texel is a debris particle)
			var WIDTH = hash || 64;

			var container, stats;
			var camera, scene, renderer, geometry, controls;

			var PARTICLES = WIDTH * WIDTH;

			document.getElementById( 'protoplanets' ).innerText = PARTICLES;

			function change(n) {
				location.hash = n;
				location.reload();
				return false;
			}


			var options = '';
			for ( var i = 1; i < 8; i++ ) {
				var j = Math.pow( 2, i );
				options += '<a href="#" onclick="return change(' + j + ')">' + ( j * j ) + '</a> ';
			}
			document.getElementById( 'options' ).innerHTML = options;

			var last = performance.now();

			var gpuCompute;
			var velocityVariable;
			var positionVariable;
			var positionUniforms;
			var velocityUniforms;
			var particleUniforms;
			var effectController;

			init();
			animate();

			function init() {

				container = document.createElement( 'div' );
				document.body.appendChild( container );

				camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 5, 15000 );
				camera.position.y = 120;
				camera.position.z = 400;

				scene = new THREE.Scene();

				renderer = new THREE.WebGLRenderer();
				renderer.setClearColor( 0x000000 );
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				container.appendChild( renderer.domElement );

				controls = new THREE.OrbitControls( camera, renderer.domElement );

				effectController = {
					// Can be changed dynamically
					gravityConstant: 100.0,
					density: 0.45,

					// Must restart simulation
					radius: 300,
					height: 8,
					exponent: 0.4,
					maxMass: 15.0,
					velocity: 70,
					velocityExponent: 0.2,
					randVelocity: 0.001

				};

				initComputeRenderer();

				stats = new Stats();
				container.appendChild( stats.dom );

				window.addEventListener( 'resize', onWindowResize, false );

				initGUI();

				initProtoplanets();

				dynamicValuesChanger();

			}

			function initComputeRenderer() {

    				gpuCompute = new GPUComputationRenderer( WIDTH, WIDTH, renderer );

				var dtPosition = gpuCompute.createTexture();
				var dtVelocity = gpuCompute.createTexture();

				fillTextures( dtPosition, dtVelocity );

				velocityVariable = gpuCompute.addVariable( "textureVelocity", document.getElementById( 'computeShaderVelocity' ).textContent, dtVelocity );
				positionVariable = gpuCompute.addVariable( "texturePosition", document.getElementById( 'computeShaderPosition' ).textContent, dtPosition );

				gpuCompute.setVariableDependencies( velocityVariable, [ positionVariable, velocityVariable ] );
				gpuCompute.setVariableDependencies( positionVariable, [ positionVariable, velocityVariable ] );

				positionUniforms = positionVariable.material.uniforms;
				velocityUniforms = velocityVariable.material.uniforms;

				velocityUniforms.gravityConstant = { value: 0.0 };
				velocityUniforms.density = { value: 0.0 };

				var error = gpuCompute.init();
				if ( error !== null ) {
				    console.error( error );
				}

			}

			function restartSimulation() {

				var dtPosition = gpuCompute.createTexture();
				var dtVelocity = gpuCompute.createTexture();

				fillTextures( dtPosition, dtVelocity );

				gpuCompute.renderTexture( dtPosition, positionVariable.renderTargets[ 0 ] );
				gpuCompute.renderTexture( dtPosition, positionVariable.renderTargets[ 1 ] );
				gpuCompute.renderTexture( dtVelocity, velocityVariable.renderTargets[ 0 ] );
				gpuCompute.renderTexture( dtVelocity, velocityVariable.renderTargets[ 1 ] );

			}

			function initProtoplanets() {

				geometry = new THREE.BufferGeometry();

				var positions = new Float32Array( PARTICLES * 3 );
				var p = 0;
				for ( var i = 0; i < PARTICLES; i++ ) {

					positions[ p++ ] = ( Math.random() * 2 - 1 ) * effectController.radius;
					positions[ p++ ] = 0;//( Math.random() * 2 - 1 ) * effectController.radius;
					positions[ p++ ] = ( Math.random() * 2 - 1 ) * effectController.radius;

				}

				var uvs = new Float32Array( PARTICLES * 2 );
				p = 0;
				for ( var j = 0; j < WIDTH; j++ ) {
					for ( var i = 0; i < WIDTH; i++ ) {

						uvs[ p++ ] = i / ( WIDTH - 1 );
						uvs[ p++ ] = j / ( WIDTH - 1 );

					}
				}

				geometry.addAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );
				geometry.addAttribute( 'uv', new THREE.BufferAttribute( uvs, 2 ) );

				particleUniforms = {
					texturePosition: { value: null },
					textureVelocity: { value: null },
					cameraConstant: { value: getCameraConstant( camera ) },
					density: { value: 0.0 }
				};

				// ShaderMaterial
				var material = new THREE.ShaderMaterial( {
					uniforms:       particleUniforms,
					vertexShader:   document.getElementById( 'particleVertexShader' ).textContent,
					fragmentShader: document.getElementById( 'particleFragmentShader' ).textContent
				} );
				material.extensions.drawBuffers = true;

				var particles = new THREE.Points( geometry, material );
				particles.matrixAutoUpdate = false;
				particles.updateMatrix();

				scene.add( particles );

			}

			function fillTextures( texturePosition, textureVelocity ) {

				var posArray = texturePosition.image.data;
				var velArray = textureVelocity.image.data;

				var radius = effectController.radius;
				var height = effectController.height;
				var exponent = effectController.exponent;
				var maxMass = effectController.maxMass * 1024 / PARTICLES;
				var maxVel = effectController.velocity;
				var velExponent = effectController.velocityExponent;
				var randVel = effectController.randVelocity;

				for ( var k = 0, kl = posArray.length; k < kl; k += 4 ) {

					// Position
					var x, y, z, rr;

					do {
						x = ( Math.random() * 2 - 1 );
						z = ( Math.random() * 2 - 1 );
						rr = x * x + z * z;
					} while ( rr > 1 );

					rr = Math.sqrt( rr );

					var rExp = radius * Math.pow( rr, exponent );

					// Velocity
					var vel = maxVel * Math.pow( rr, velExponent );

					var vx = vel * z + ( Math.random() * 2 - 1 ) * randVel;
					var vy = ( Math.random() * 2 - 1 ) * randVel * 0.05;
					var vz = - vel * x + ( Math.random() * 2 - 1 ) * randVel;

					x *= rExp;
					z *= rExp;
					y = ( Math.random() * 2 - 1 ) * height;

					var mass = Math.random() * maxMass + 1;

					// Fill in texture values
					posArray[ k + 0 ] = x;
					posArray[ k + 1 ] = y;
					posArray[ k + 2 ] = z;
					posArray[ k + 3 ] = 1;

					velArray[ k + 0 ] = vx;
					velArray[ k + 1 ] = vy;
					velArray[ k + 2 ] = vz;
					velArray[ k + 3 ] = mass;

				}

			}

			function onWindowResize() {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );

				particleUniforms.cameraConstant.value = getCameraConstant( camera );

    			}

			function dynamicValuesChanger() {

				velocityUniforms.gravityConstant.value = effectController.gravityConstant;
				velocityUniforms.density.value = effectController.density;
				particleUniforms.density.value = effectController.density;

			}

			function initGUI() {

  				var gui = new dat.GUI();

				var folder1 = gui.addFolder( 'Dynamic parameters' );

				folder1.add( effectController, "gravityConstant", 0.0, 1000.0, 0.05 ).onChange( dynamicValuesChanger );
				folder1.add( effectController, "density", 0.0, 10.0, 0.001 ).onChange( dynamicValuesChanger );

				var folder2 = gui.addFolder( 'Static parameters - press restartSimulation' );

				folder2.add( effectController, "radius", 10.0, 1000.0, 1.0 );
				folder2.add( effectController, "height", 0.0, 50.0, 0.01 );
				folder2.add( effectController, "exponent", 0.0, 2.0, 0.001 );
				folder2.add( effectController, "maxMass", 1.0, 50.0, 0.1 );
				folder2.add( effectController, "velocity", 0.0, 150.0, 0.1 );
				folder2.add( effectController, "velocityExponent", 0.0, 1.0, 0.01 );
				folder2.add( effectController, "randVelocity", 0.0, 50.0, 0.1 );

				var buttonRestart = {
				    restartSimulation: function() {
					restartSimulation();
				    }
				};
				folder2.add( buttonRestart, 'restartSimulation' );

				folder1.open();
				folder2.open();

			}

			function getCameraConstant( camera ) {

				return window.innerHeight / ( Math.tan( THREE.Math.DEG2RAD * 0.5 * camera.fov ) / camera.zoom );

			}


			function animate() {

				requestAnimationFrame( animate );

				render();
				stats.update();

			}

			function render() {

				var now = performance.now();
				var delta = (now - last) / 1000;

				if (delta > 1) delta = 1; // safety cap on large deltas
				last = now;

				gpuCompute.compute();

				particleUniforms.texturePosition.value = gpuCompute.getCurrentRenderTarget( positionVariable ).texture;
				particleUniforms.textureVelocity.value = gpuCompute.getCurrentRenderTarget( velocityVariable ).texture;

				renderer.render( scene, camera );

			}

		</script>
	</body>
</html>
